如何优化你的if-else?来试试“责任树模式”
阿里妹导读:写业务逻辑时,if-else 可能是最容易想到的逻辑方式了。然而大量堆砌的 if-else 毫无疑问将给代码维护带来巨大的困难。如何优化这些 if-else 呢?本文分享一种设计模式——责任树模式,通过将责任链与策略模式融合,成为一种广义的责任链模式,不仅可以完成任务的逐级委托,也可以在任一级选择不同的下游策略进行处理,并将责任树模式抽象出一个通用的框架。
扪心自问,你在写业务代码时是不是也习惯狂堆 if-else 呢?
如何解决接口升级,在保证兼容老版本的情况下轻松开发新版本业务逻辑?
如何根据入参 p1、p2、p3 等的不同组合进行策略定位?
后续迭代人力成本降低。
代码结构更清晰,可维护性提升:没有了各种“卫语句”的跳转 & 维护性巨差的巨型方法,函数可以收敛在理想的 50 行内。
后续新增需求修改代码不易出错:策略间隔离,不需要完整看一遍大函数理清逻辑再修改,只需要无脑添加一条路由 + 新的策略实现方法即可。
问题易定位:同样由于策略间隔离,调试时可以直接定位到指定策略的业务逻辑代码,不需要逐句排查。
Router 是一个抽象类,负责定义如何路由到下游的多个子节点。
Handler 是接口,负责实现每个节点的业务逻辑。
除了根节点(入口)外,每个节点都实现了 Handler 接口。根节点只继承 Router 抽象类。
所有叶子节点只实现 Handler 接口而无需继承 Router 抽象类(无需再向下委托)。
除了根节点和叶子节点外的其他节点,都是上一层的 Handler,同时是下一层的 Router。
/**
* 通用的“策略树“框架,通过树形结构实现分发与委托,每层通过指定的参数进行向下分发委托,直到达到最终的执行者。
* 该框架包含两个类:{@code StrategyHandler} 和 {@code AbstractStrategyRouter}
* 其中:通过实现 {@code AbstractStrategyRouter} 抽象类完成对策略的分发,
* 实现 {@code StrategyHandler} 接口来对策略进行实现。
* 像是第二层 A、B 这样的节点,既是 Root 节点的策略实现者也是策略A1、A2、B1、B2 的分发者,这样的节点只需要
* 同时继承 {@code StrategyHandler} 和实现 {@code AbstractStrategyRouter} 接口就可以了。
*
* <pre>
* +---------+
* | Root | ----------- 第 1 层策略入口
* +---------+
* / \ ------------- 根据入参 P1 进行策略分发
* / \
* +------+ +------+
* | A | | B | ------- 第 2 层不同策略的实现
* +------+ +------+
* / \ / \ --------- 根据入参 P2 进行策略分发
* / \ / \
* +---+ +---+ +---+ +---+
* |A1 | |A2 | |B1 | |B2 | ----- 第 3 层不同策略的实现
* +---+ +---+ +---+ +---+
* </pre>
*
* @author
* @date
* @see StrategyHandler
*/
@Component
public abstract class AbstractStrategyRouter<T, R> {
/**
* 策略映射器,根据指定的入参路由到对应的策略处理者。
*
* @param <T> 策略的入参类型
* @param <R> 策略的返回值类型
*/
public interface StrategyMapper<T, R> {
/**
* 根据入参获取到对应的策略处理者。可通过 if-else 实现,也可通过 Map 实现。
*
* @param param 入参
* @return 策略处理者
*/
StrategyHandler<T, R> get(T param);
}
private StrategyMapper<T, R> strategyMapper;
/**
* 类初始化时注册分发策略 Mapper
*/
@PostConstruct
private void abstractInit() {
strategyMapper = registerStrategyMapper();
Objects.requireNonNull(strategyMapper, "strategyMapper cannot be null");
}
@Getter
@Setter
@SuppressWarnings("unchecked")
private StrategyHandler<T, R> defaultStrategyHandler = StrategyHandler.DEFAULT;
/**
* 执行策略,框架会自动根据策略分发至下游的 Handler 进行处理
*
* @param param 入参
* @return 下游执行者给出的返回值
*/
public R applyStrategy(T param) {
final StrategyHandler<T, R> strategyHandler = strategyMapper.get(param);
if (strategyHandler != null) {
return strategyHandler.apply(param);
}
return defaultStrategyHandler.apply(param);
}
/**
* 抽象方法,需要子类实现策略的分发逻辑
*
* @return 分发逻辑 Mapper 对象
*/
protected abstract StrategyMapper<T, R> registerStrategyMapper();
}
/**
* @author
* @date
*/
public interface StrategyHandler<T, R> {
@SuppressWarnings("rawtypes")
StrategyHandler DEFAULT = t -> null;
/**
* apply strategy
*
* @param param
* @return
*/
R apply(T param);
}
we should forget about small efficiencies,say about 97% of the time:premature optimization is the root of all evil.
任何“结论”都有其所处背景、上下文细节等,通过一句话指导工作是不成立的。优秀的架构师可以给出架构设计是在理论基础、大量实践、不断思考总结以及无数采坑的经验的基础上得来的,而不是他知道一句别人都不知道的“咒语”。
Knuth 这句话更偏重于反对奇技淫巧、细枝末节的性能优化,因为在“过早”的时候无法准确获知系统的瓶颈且局部的优化不仅不能带来收益,反而会造成更大的代价。他批评的恰恰是不着眼于整体架构的局部视角对系统的破坏,而架构设计正是需要从整体视角去做选择与权衡。因此将 Knuth 这句话直接推广到“架构设计”上并不妥当。
很多人觉得在项目开发时需求经常“瞬息万变”、“朝令夕改”,而做优化又需要花费大量时间思考,根本没有精力优化。我认为这种论述也是不成立的,凭什么认为等到坏味道严重、历史包袱沉重的时候就有精力、能力和胆量做优化了呢?
何时是所谓的“不早”很难界定,其实我们永远都无法确定自己掌握了足够的细节可以进行绝对正确的优化。在现实世界中,受到时间维度的限制,我们永远无法达成全局最优,只能以局部最优不断去逼近全局最优。我觉得等到坏味道严重不得不重构的时候才想起优化已为时过晚。
这句话不应该成为不做设计的借口,即使最终提交的代码仍是“if-else”版本,也不应省略思考、推演、权衡的过程,日常需求是练兵场,是精进技术的必经之路。
设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案,是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了重用代码,让代码更容易被他人理解,保证代码的可靠性。本课程共36个课时,为同学们详解设计模式。
点击“阅读原文”,快去学习吧~